ปลดล็อกประสบการณ์ผู้ใช้ที่ลื่นไหลด้วยการทำความเข้าใจการจัดการช่องทางลำดับความสำคัญของ React Fiber คู่มือฉบับสมบูรณ์เกี่ยวกับ Concurrent Rendering, Scheduler และ API ใหม่อย่าง startTransition
การจัดการช่องทางลำดับความสำคัญของ React Fiber: เจาะลึกการควบคุมการเรนเดอร์
ในโลกของการพัฒนาเว็บ ประสบการณ์ผู้ใช้คือสิ่งสำคัญที่สุด การค้างชั่วขณะ, แอนิเมชันที่กระตุก, หรือช่องกรอกข้อมูลที่หน่วง อาจเป็นตัวตัดสินระหว่างผู้ใช้ที่พึงพอใจกับผู้ใช้ที่หงุดหงิด หลายปีที่ผ่านมา นักพัฒนาต้องต่อสู้กับธรรมชาติการทำงานแบบเธรดเดียวของเบราว์เซอร์เพื่อสร้างแอปพลิเคชันที่ลื่นไหลและตอบสนองได้ดี แต่ด้วยการมาถึงของสถาปัตยกรรม Fiber ใน React 16 และการใช้งานอย่างเต็มรูปแบบด้วย Concurrent Features ใน React 18 กฎเกณฑ์ก็ได้เปลี่ยนไปโดยสิ้นเชิง React ได้พัฒนาจากไลบรารีที่ทำหน้าที่เพียงเรนเดอร์ UI ไปสู่ไลบรารีที่สามารถ จัดตารางเวลา การอัปเดต UI ได้อย่างชาญฉลาด
บทความเจาะลึกนี้จะสำรวจหัวใจของการพัฒนานี้ นั่นคือการจัดการช่องทางลำดับความสำคัญของ React Fiber เราจะไขข้อข้องใจว่า React ตัดสินใจอย่างไรว่าจะเรนเดอร์อะไรในตอนนี้, อะไรที่รอได้, และจะจัดการกับการอัปเดต state หลายๆ อย่างพร้อมกันโดยไม่ทำให้ UI ค้างได้อย่างไร นี่ไม่ใช่แค่การศึกษาเชิงทฤษฎี การทำความเข้าใจหลักการสำคัญเหล่านี้จะช่วยให้คุณสร้างแอปพลิเคชันที่เร็วขึ้น, ฉลาดขึ้น, และยืดหยุ่นมากขึ้นสำหรับผู้ใช้ทั่วโลก
จาก Stack Reconciler สู่ Fiber: 'เหตุผล' เบื้องหลังการเขียนใหม่ทั้งหมด
เพื่อให้เห็นคุณค่าของนวัตกรรมอย่าง Fiber เราต้องเข้าใจข้อจำกัดของรุ่นก่อนหน้าอย่าง Stack Reconciler ก่อน React 16 กระบวนการ reconciliation ซึ่งเป็นอัลกอริทึมที่ React ใช้เปรียบเทียบ tree หนึ่งกับอีก tree หนึ่งเพื่อตัดสินใจว่าจะเปลี่ยนแปลงอะไรใน DOM นั้น ทำงานแบบซิงโครนัสและเรียกซ้ำ (synchronous and recursive) เมื่อ state ของคอมโพเนนต์อัปเดต React จะไล่ไปตาม component tree ทั้งหมด, คำนวณการเปลี่ยนแปลง, และนำไปใช้กับ DOM ในลำดับเดียวที่ไม่สามารถขัดจังหวะได้
สำหรับแอปพลิเคชันขนาดเล็ก วิธีนี้ก็ใช้ได้ดี แต่สำหรับ UI ที่ซับซ้อนซึ่งมี component tree ที่ลึก กระบวนการนี้อาจใช้เวลานาน เช่น มากกว่า 16 มิลลิวินาที เนื่องจาก JavaScript เป็น single-threaded งาน reconciliation ที่ใช้เวลานานจะบล็อกเธรดหลัก ซึ่งหมายความว่าเบราว์เซอร์จะไม่สามารถจัดการงานสำคัญอื่นๆ ได้ เช่น:
- การตอบสนองต่อการกระทำของผู้ใช้ (เช่น การพิมพ์หรือการคลิก)
- การเล่นแอนิเมชัน (ทั้ง CSS และ JavaScript)
- การทำงานของลอจิกอื่นๆ ที่ต้องตรงต่อเวลา
ผลลัพธ์ที่ได้คือปรากฏการณ์ที่เรียกว่า "jank" หรืออาการกระตุก ซึ่งทำให้ประสบการณ์ผู้ใช้ไม่ลื่นไหลและไม่ตอบสนอง Stack Reconciler ทำงานเหมือนทางรถไฟรางเดี่ยว เมื่อรถไฟขบวนหนึ่ง (การอัปเดตการเรนเดอร์) เริ่มเดินทาง มันต้องวิ่งไปจนสุดทางและไม่มีรถไฟขบวนอื่นสามารถใช้รางได้ ธรรมชาติที่ปิดกั้นนี้เป็นแรงจูงใจหลักในการเขียนอัลกอริทึมหลักของ React ใหม่ทั้งหมด
แนวคิดหลักเบื้องหลัง React Fiber คือการจินตนาการกระบวนการ reconciliation ใหม่ให้เป็นสิ่งที่สามารถแบ่งออกเป็นงานชิ้นเล็กๆ ได้ แทนที่จะเป็นงานขนาดใหญ่ชิ้นเดียว การเรนเดอร์สามารถหยุดชั่วคราว, ทำต่อ, หรือแม้กระทั่งยกเลิกได้ การเปลี่ยนแปลงจากกระบวนการที่ซิงโครนัสไปสู่กระบวนการที่อะซิงโครนัสและจัดตารางเวลาได้นี้ ทำให้ React สามารถคืนการควบคุมกลับไปยังเธรดหลักของเบราว์เซอร์ได้ เพื่อให้แน่ใจว่างานที่มีลำดับความสำคัญสูงอย่างการรับข้อมูลจากผู้ใช้จะไม่ถูกบล็อก Fiber ได้เปลี่ยนทางรถไฟรางเดี่ยวให้กลายเป็นทางด่วนหลายเลนที่มีช่องทางพิเศษสำหรับรถที่ต้องการความเร็วสูง
'Fiber' คืออะไร? ส่วนประกอบพื้นฐานของ Concurrency
โดยแก่นแท้แล้ว "fiber" คืออ็อบเจ็กต์ JavaScript ที่แทน "หน่วยของงาน" (unit of work) มันมีข้อมูลเกี่ยวกับคอมโพเนนต์, อินพุต (props), และเอาต์พุต (children) ของมัน คุณสามารถคิดว่า fiber เป็นเหมือน virtual stack frame ใน Stack Reconciler แบบเก่า จะใช้ call stack ของเบราว์เซอร์ในการจัดการการท่องไปใน tree แบบเรียกซ้ำ แต่ด้วย Fiber, React ได้สร้าง virtual stack ของตัวเองขึ้นมา ซึ่งอยู่ในรูปของ linked list ของ fiber node สิ่งนี้ทำให้ React ควบคุมกระบวนการเรนเดอร์ได้อย่างสมบูรณ์
ทุกๆ element ใน component tree ของคุณจะมี fiber node ที่สอดคล้องกัน node เหล่านี้เชื่อมต่อกันเป็น fiber tree ซึ่งจำลองโครงสร้างของ component tree โดย fiber node จะเก็บข้อมูลสำคัญต่างๆ รวมถึง:
- type และ key: ตัวระบุสำหรับคอมโพเนนต์ คล้ายกับที่คุณเห็นใน React element
- child: พอยน์เตอร์ชี้ไปยัง child fiber ตัวแรก
- sibling: พอยน์เตอร์ชี้ไปยัง sibling fiber ตัวถัดไป
- return: พอยน์เตอร์ชี้ไปยัง parent fiber (เส้นทาง 'กลับ' หลังจากทำงานเสร็จ)
- pendingProps และ memoizedProps: Props จากการเรนเดอร์ครั้งก่อนและครั้งถัดไป ใช้สำหรับการเปรียบเทียบ (diffing)
- stateNode: การอ้างอิงถึง DOM node จริง, อินสแตนซ์ของคลาส, หรือ element พื้นฐานของแพลตฟอร์ม
- effectTag: bitmask ที่อธิบายงานที่ต้องทำ (เช่น Placement, Update, Deletion)
โครงสร้างนี้ช่วยให้ React สามารถท่องไปใน tree ได้โดยไม่ต้องอาศัย native recursion มันสามารถเริ่มทำงานกับ fiber หนึ่ง, หยุดชั่วคราว, แล้วกลับมาทำต่อในภายหลังได้โดยไม่ลืมว่าทำถึงไหนแล้ว ความสามารถในการหยุดและทำต่อนี้เป็นกลไกพื้นฐานที่ทำให้ฟีเจอร์ concurrent ทั้งหมดของ React เป็นไปได้
หัวใจของระบบ: Scheduler และระดับความสำคัญ
ถ้า fiber คือหน่วยของงาน Scheduler ก็คือสมองที่ตัดสินใจว่าจะทำงานไหนและเมื่อไหร่ React ไม่ได้เริ่มเรนเดอร์ทันทีที่มีการเปลี่ยนแปลง state แต่จะกำหนดระดับความสำคัญให้กับการอัปเดตนั้นและขอให้ Scheduler จัดการให้ จากนั้น Scheduler จะทำงานร่วมกับเบราว์เซอร์เพื่อหาเวลาที่ดีที่สุดในการทำงานนั้น เพื่อให้แน่ใจว่าจะไม่ไปบล็อกงานที่สำคัญกว่า
ในตอนแรก ระบบนี้ใช้ชุดของระดับความสำคัญที่ไม่ต่อเนื่องกัน แม้ว่าการใช้งานในปัจจุบัน (โมเดล Lane) จะมีความละเอียดอ่อนกว่า แต่การทำความเข้าใจระดับตามแนวคิดเหล่านี้ก็เป็นจุดเริ่มต้นที่ดี:
- ImmediatePriority: นี่คือลำดับความสำคัญสูงสุด สงวนไว้สำหรับการอัปเดตแบบซิงโครนัสที่ต้องเกิดขึ้นทันที ตัวอย่างคลาสสิกคือ controlled input เมื่อผู้ใช้พิมพ์ในช่องกรอกข้อมูล UI ต้องสะท้อนการเปลี่ยนแปลงนั้นทันที หากล่าช้าไปเพียงไม่กี่มิลลิวินาที การพิมพ์จะรู้สึกหน่วง
- UserBlockingPriority: สำหรับการอัปเดตที่เป็นผลมาจากการกระทำของผู้ใช้โดยตรง เช่น การคลิกปุ่มหรือการแตะหน้าจอ สิ่งเหล่านี้ควรให้ความรู้สึกว่าเกิดขึ้นทันทีสำหรับผู้ใช้ แต่สามารถเลื่อนออกไปได้เล็กน้อยหากจำเป็น event handler ส่วนใหญ่จะทริกเกอร์การอัปเดตที่ลำดับความสำคัญนี้
- NormalPriority: นี่คือลำดับความสำคัญเริ่มต้นสำหรับการอัปเดตส่วนใหญ่ เช่น การอัปเดตที่มาจากการดึงข้อมูล (`useEffect`) หรือการนำทาง (navigation) การอัปเดตเหล่านี้ไม่จำเป็นต้องเกิดขึ้นทันที และ React สามารถจัดตารางเวลาเพื่อหลีกเลี่ยงการรบกวนการโต้ตอบของผู้ใช้
- LowPriority: สำหรับการอัปเดตที่ไม่สำคัญต่อเวลา เช่น การเรนเดอร์เนื้อหาที่อยู่นอกหน้าจอ หรือ analytics events
- IdlePriority: ลำดับความสำคัญต่ำสุด สำหรับงานที่สามารถทำได้เมื่อเบราว์เซอร์ว่างสนิทเท่านั้น ส่วนนี้ไม่ค่อยได้ถูกใช้โดยตรงในโค้ดของแอปพลิเคชัน แต่ใช้ภายในสำหรับสิ่งต่างๆ เช่น การบันทึก log หรือการคำนวณงานล่วงหน้า
React จะกำหนดลำดับความสำคัญที่ถูกต้องโดยอัตโนมัติตามบริบทของการอัปเดต ตัวอย่างเช่น การอัปเดตภายใน `click` event handler จะถูกจัดตารางเป็น `UserBlockingPriority` ในขณะที่การอัปเดตภายใน `useEffect` โดยทั่วไปจะเป็น `NormalPriority` การจัดลำดับความสำคัญที่ชาญฉลาดและรับรู้บริบทนี้คือสิ่งที่ทำให้ React รู้สึกเร็วตั้งแต่แกะกล่อง
ทฤษฎีเลน (Lane Theory): โมเดลลำดับความสำคัญยุคใหม่
เมื่อฟีเจอร์ concurrent ของ React มีความซับซ้อนมากขึ้น ระบบลำดับความสำคัญแบบตัวเลขธรรมดาก็พิสูจน์แล้วว่าไม่เพียงพอ มันไม่สามารถจัดการกับสถานการณ์ที่ซับซ้อนได้อย่างสง่างาม เช่น การอัปเดตหลายรายการที่มีลำดับความสำคัญต่างกัน, การขัดจังหวะ, และการจัดกลุ่ม (batching) สิ่งนี้นำไปสู่การพัฒนา **โมเดลเลน (Lane model)**
แทนที่จะเป็นตัวเลขลำดับความสำคัญเดียว ให้คิดว่าเป็นชุดของ "เลน" 31 เลน แต่ละเลนแทนลำดับความสำคัญที่แตกต่างกัน สิ่งนี้ถูกนำไปใช้เป็น bitmask ซึ่งเป็นจำนวนเต็ม 31 บิต โดยแต่ละบิตจะสอดคล้องกับเลนหนึ่งเลน วิธีการ bitmask นี้มีประสิทธิภาพสูงและช่วยให้สามารถดำเนินการที่มีประสิทธิภาพได้:
- การแสดงลำดับความสำคัญหลายระดับ: bitmask เดียวสามารถแทนชุดของลำดับความสำคัญที่รอการดำเนินการได้ ตัวอย่างเช่น หากมีการอัปเดตทั้งแบบ `UserBlocking` และ `Normal` รออยู่ในคอมโพเนนต์ คุณสมบัติ `lanes` ของมันจะมีบิตสำหรับทั้งสองลำดับความสำคัญนี้ถูกตั้งค่าเป็น 1
- การตรวจสอบการทับซ้อน: การดำเนินการระดับบิต (Bitwise operations) ทำให้การตรวจสอบว่าชุดเลนสองชุดทับซ้อนกันหรือไม่ หรือชุดหนึ่งเป็นซับเซตของอีกชุดหนึ่งเป็นเรื่องง่าย สิ่งนี้ใช้เพื่อตัดสินว่าการอัปเดตที่เข้ามาใหม่สามารถจัดกลุ่มเข้ากับงานที่มีอยู่ได้หรือไม่
- การจัดลำดับความสำคัญของงาน: React สามารถระบุเลนที่มีลำดับความสำคัญสูงสุดในชุดของเลนที่รอดำเนินการได้อย่างรวดเร็ว และเลือกที่จะทำงานเฉพาะเลนนั้นก่อน โดยไม่สนใจงานที่มีลำดับความสำคัญต่ำกว่าในขณะนั้น
อาจเปรียบได้กับสระว่ายน้ำที่มี 31 เลน การอัปเดตเร่งด่วนเปรียบเหมือนนักว่ายน้ำแข่งขัน จะได้เลนที่มีลำดับความสำคัญสูงและสามารถว่ายไปได้โดยไม่มีการขัดจังหวะ การอัปเดตที่ไม่เร่งด่วนหลายรายการเปรียบเหมือนนักว่ายน้ำทั่วไป อาจถูกจัดกลุ่มไว้ในเลนที่มีลำดับความสำคัญต่ำกว่า หากมีนักว่ายน้ำแข่งขันมาถึงกะทันหัน ไลฟ์การ์ด (Scheduler) สามารถหยุดนักว่ายน้ำทั่วไปชั่วคราวเพื่อให้ผู้ที่สำคัญกว่าผ่านไปได้ โมเดลเลนทำให้ React มีระบบที่ละเอียดและยืดหยุ่นสูงสำหรับการจัดการการประสานงานที่ซับซ้อนนี้
กระบวนการ Reconciliation แบบสองเฟส
ความมหัศจรรย์ของ React Fiber เกิดขึ้นได้ผ่านสถาปัตยกรรม commit แบบสองเฟส การแยกส่วนนี้คือสิ่งที่ทำให้การเรนเดอร์สามารถถูกขัดจังหวะได้โดยไม่ทำให้เกิดความไม่สอดคล้องกันทางภาพ
เฟสที่ 1: เฟสเรนเดอร์/Reconciliation (ทำงานแบบอะซิงโครนัสและขัดจังหวะได้)
นี่คือช่วงที่ React ทำงานหนัก โดยเริ่มจากรากของ component tree, React จะท่องไปตาม fiber node ใน `workLoop` สำหรับแต่ละ fiber มันจะพิจารณาว่าจำเป็นต้องอัปเดตหรือไม่ มันจะเรียกคอมโพเนนท์ของคุณ, เปรียบเทียบ element ใหม่กับ fiber เก่า, และสร้างรายการของ side effects ขึ้นมา (เช่น "เพิ่ม DOM node นี้", "อัปเดต attribute นี้", "ลบคอมโพเนนท์นี้")
คุณสมบัติที่สำคัญของเฟสนี้คือมัน ทำงานแบบอะซิงโครนัสและสามารถถูกขัดจังหวะได้ หลังจากประมวลผล fiber ไปสองสามตัว React จะตรวจสอบว่าหมดเวลาที่จัดสรรไว้หรือยัง (ปกติคือไม่กี่มิลลิวินาที) ผ่านฟังก์ชันภายในที่เรียกว่า `shouldYield` หากมีเหตุการณ์ที่มีลำดับความสำคัญสูงกว่าเกิดขึ้น (เช่น การกระทำของผู้ใช้) หรือถ้าหมดเวลาแล้ว React จะหยุดทำงานชั่วคราว, บันทึกความคืบหน้าไว้ใน fiber tree, และคืนการควบคุมกลับไปยังเธรดหลักของเบราว์เซอร์ เมื่อเบราว์เซอร์ว่างอีกครั้ง React ก็สามารถกลับมาทำงานต่อจากจุดที่ค้างไว้ได้เลย
ตลอดทั้งเฟสนี้ จะไม่มีการเปลี่ยนแปลงใดๆ ถูกส่งไปยัง DOM ผู้ใช้จะยังคงเห็น UI เดิมที่สอดคล้องกันอยู่ นี่เป็นสิ่งสำคัญมาก เพราะถ้า React ใช้การเปลี่ยนแปลงทีละส่วน ผู้ใช้จะเห็นอินเทอร์เฟซที่พังและเรนเดอร์ไม่สมบูรณ์ การเปลี่ยนแปลงทั้งหมดจะถูกคำนวณและรวบรวมไว้ในหน่วยความจำเพื่อรอเฟสคอมมิต
เฟสที่ 2: เฟสคอมมิต (ทำงานแบบซิงโครนัสและขัดจังหวะไม่ได้)
เมื่อเฟสเรนเดอร์เสร็จสมบูรณ์สำหรับ tree ที่อัปเดตทั้งหมดโดยไม่มีการขัดจังหวะ React จะเข้าสู่เฟสคอมมิต ในเฟสนี้ มันจะนำรายการ side effects ที่รวบรวมไว้มาใช้กับ DOM
เฟสนี้ ทำงานแบบซิงโครนัสและไม่สามารถขัดจังหวะได้ มันจำเป็นต้องทำงานให้เสร็จในครั้งเดียวอย่างรวดเร็วเพื่อให้แน่ใจว่า DOM ได้รับการอัปเดตอย่างสมบูรณ์ในคราวเดียว (atomically) ซึ่งจะป้องกันไม่ให้ผู้ใช้เห็น UI ที่ไม่สอดคล้องหรืออัปเดตเพียงบางส่วน นี่เป็นช่วงเวลาที่ React เรียกใช้ lifecycle methods เช่น `componentDidMount` และ `componentDidUpdate` รวมถึง `useLayoutEffect` hook ด้วย เนื่องจากมันทำงานแบบซิงโครนัส คุณจึงควรหลีกเลี่ยงโค้ดที่ทำงานนานใน `useLayoutEffect` เพราะมันสามารถบล็อกการแสดงผล (painting) ได้
หลังจากเฟสคอมมิตเสร็จสิ้นและ DOM ได้รับการอัปเดตแล้ว React จะจัดตารางให้ `useEffect` hook ทำงานแบบอะซิงโครนัส ซึ่งทำให้แน่ใจได้ว่าโค้ดใดๆ ภายใน `useEffect` (เช่น การดึงข้อมูล) จะไม่บล็อกเบราว์เซอร์จากการแสดงผล UI ที่อัปเดตแล้วบนหน้าจอ
การนำไปใช้จริงและการควบคุมผ่าน API
การเข้าใจทฤษฎีเป็นสิ่งที่ดี แต่แล้วนักพัฒนาในทีมระดับโลกจะใช้ประโยชน์จากระบบอันทรงพลังนี้ได้อย่างไร? React 18 ได้นำเสนอ API หลายตัวที่ให้นักพัฒนาสามารถควบคุมลำดับความสำคัญของการเรนเดอร์ได้โดยตรง
Automatic Batching
ใน React 18 การอัปเดต state ทั้งหมดจะถูกจัดกลุ่ม (batch) โดยอัตโนมัติ ไม่ว่าจะเกิดขึ้นจากที่ใดก็ตาม ก่อนหน้านี้ มีเพียงการอัปเดตภายใน React event handler เท่านั้นที่ถูกจัดกลุ่ม การอัปเดตภายใน promises, `setTimeout`, หรือ native event handler จะทริกเกอร์การ re-render แยกกันทุกครั้ง แต่ตอนนี้ ต้องขอบคุณ Scheduler ที่ทำให้ React รอหนึ่ง "tick" และจัดกลุ่มการอัปเดต state ทั้งหมดที่เกิดขึ้นภายใน tick นั้นให้เป็นการ re-render ที่ปรับให้เหมาะสมเพียงครั้งเดียว ซึ่งช่วยลดการเรนเดอร์ที่ไม่จำเป็นและปรับปรุงประสิทธิภาพโดยปริยาย
`startTransition` API
นี่อาจเป็น API ที่สำคัญที่สุดสำหรับการควบคุมลำดับความสำคัญในการเรนเดอร์ `startTransition` ช่วยให้คุณสามารถทำเครื่องหมายการอัปเดต state บางอย่างว่าไม่เร่งด่วน หรือเป็น "transition"
ลองนึกภาพช่องค้นหา เมื่อผู้ใช้พิมพ์ จะมีสองสิ่งที่ต้องเกิดขึ้น: 1. ตัวช่องค้นหาเองต้องอัปเดตเพื่อแสดงตัวอักษรใหม่ (ลำดับความสำคัญสูง) 2. รายการผลการค้นหาต้องถูกกรองและ re-render ซึ่งอาจเป็นกระบวนการที่ช้า (ลำดับความสำคัญต่ำ)
หากไม่มี `startTransition` การอัปเดตทั้งสองจะมีลำดับความสำคัญเท่ากัน และรายการที่เรนเดอร์ช้าอาจทำให้ช่องค้นหาหน่วง สร้างประสบการณ์ผู้ใช้ที่ไม่ดี แต่ด้วยการครอบการอัปเดตรายการด้วย `startTransition` คุณกำลังบอก React ว่า: "การอัปเดตนี้ไม่สำคัญมาก สามารถแสดงรายการเก่าไปก่อนชั่วขณะระหว่างที่เตรียมรายการใหม่ได้ ให้ความสำคัญกับการทำให้ช่องค้นหาตอบสนองได้ดีก่อน"
นี่คือตัวอย่างการใช้งาน:
กำลังโหลดผลการค้นหา...
import { useState, useTransition } from 'react';
function SearchPage() {
const [isPending, startTransition] = useTransition();
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const handleInputChange = (e) => {
// อัปเดตลำดับความสำคัญสูง: อัปเดตช่อง input ทันที
setInputValue(e.target.value);
// อัปเดตลำดับความสำคัญต่ำ: ครอบการอัปเดต state ที่ช้าด้วย transition
startTransition(() => {
setSearchQuery(e.target.value);
});
};
return (
ในโค้ดนี้ `setInputValue` เป็นการอัปเดตที่มีลำดับความสำคัญสูง ทำให้มั่นใจได้ว่า input จะไม่หน่วง ส่วน `setSearchQuery` ซึ่งทริกเกอร์ให้คอมโพเนนต์ `SearchResults` ที่อาจจะช้า re-render ถูกทำเครื่องหมายว่าเป็น transition ทำให้ React สามารถขัดจังหวะ transition นี้ได้หากผู้ใช้พิมพ์อีกครั้ง โดยทิ้งงานเรนเดอร์ที่ล้าสมัยไปและเริ่มต้นใหม่ด้วย query ใหม่ แฟล็ก `isPending` ที่ได้จาก `useTransition` hook เป็นวิธีที่สะดวกในการแสดงสถานะการโหลดให้ผู้ใช้เห็นระหว่าง transition นี้
`useDeferredValue` Hook
`useDeferredValue` เป็นอีกวิธีหนึ่งที่ให้ผลลัพธ์คล้ายกัน มันช่วยให้คุณเลื่อนการ re-render ของส่วนที่ไม่สำคัญของ tree ออกไป มันเหมือนกับการใช้ debounce แต่ฉลาดกว่ามากเพราะมันทำงานร่วมกับ Scheduler ของ React โดยตรง
มันรับค่า (value) เข้าไปและคืนค่าสำเนาใหม่ของค่านั้นซึ่งจะ "ตามหลัง" ค่าเดิมเล็กน้อยระหว่างการเรนเดอร์ หากการเรนเดอร์ปัจจุบันถูกทริกเกอร์โดยการอัปเดตที่เร่งด่วน (เช่น การกระทำของผู้ใช้) React จะเรนเดอร์ด้วยค่า deferred เก่าก่อน แล้วจึงจัดตารางการ re-render ด้วยค่าใหม่ในลำดับความสำคัญที่ต่ำกว่า
ลองปรับปรุงตัวอย่างการค้นหาโดยใช้ `useDeferredValue`:
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const handleInputChange = (e) => {
setQuery(e.target.value);
};
return (
ในที่นี้ `input` จะอัปเดตตาม `query` ล่าสุดเสมอ แต่ `SearchResults` จะได้รับ `deferredQuery` เมื่อผู้ใช้พิมพ์เร็ว `query` จะอัปเดตทุกครั้งที่กดแป้นพิมพ์ แต่ `deferredQuery` จะยังคงค่าเดิมไว้จนกว่า React จะมีเวลาว่าง สิ่งนี้ช่วยลดลำดับความสำคัญของการเรนเดอร์รายการผลการค้นหาได้อย่างมีประสิทธิภาพ ทำให้ UI ลื่นไหล
การสร้างภาพจำลองช่องทางลำดับความสำคัญ: โมเดลในใจ
ลองมาดูสถานการณ์ที่ซับซ้อนเพื่อทำให้โมเดลในใจนี้ชัดเจนขึ้น สมมติว่าเป็นแอปพลิเคชันฟีดโซเชียลมีเดีย:
- สถานะเริ่มต้น: ผู้ใช้กำลังเลื่อนดูรายการโพสต์ยาวๆ การกระทำนี้ทริกเกอร์การอัปเดต `NormalPriority` เพื่อเรนเดอร์รายการใหม่ๆ ที่เข้ามาในมุมมอง
- การขัดจังหวะลำดับความสำคัญสูง: ขณะที่กำลังเลื่อน ผู้ใช้ตัดสินใจพิมพ์ความคิดเห็นในกล่องคอมเมนต์ของโพสต์ การพิมพ์นี้ทริกเกอร์การอัปเดต `ImmediatePriority` ที่ช่องกรอกข้อมูล
- งานลำดับความสำคัญต่ำที่ทำงานพร้อมกัน: กล่องคอมเมนต์อาจมีฟีเจอร์แสดงตัวอย่างข้อความที่จัดรูปแบบแล้วแบบสดๆ การเรนเดอร์ตัวอย่างนี้อาจช้า เราสามารถครอบการอัปเดต state สำหรับตัวอย่างนี้ด้วย `startTransition` ทำให้เป็นการอัปเดตแบบ `LowPriority`
- การอัปเดตเบื้องหลัง: ในเวลาเดียวกัน การเรียก `fetch` เพื่อดึงโพสต์ใหม่ในเบื้องหลังเสร็จสิ้น ทริกเกอร์การอัปเดต state แบบ `NormalPriority` อีกครั้งเพื่อเพิ่มแบนเนอร์ "มีโพสต์ใหม่" ที่ด้านบนของฟีด
นี่คือวิธีที่ Scheduler ของ React จะจัดการการจราจรนี้:
- React จะหยุดงานเรนเดอร์การเลื่อนที่เป็น `NormalPriority` ชั่วคราวทันที
- มันจะจัดการกับการอัปเดต input ที่เป็น `ImmediatePriority` ทันที การพิมพ์ของผู้ใช้จะรู้สึกตอบสนองอย่างสมบูรณ์
- มันจะเริ่มทำงานเรนเดอร์ตัวอย่างคอมเมนต์ที่เป็น `LowPriority` ในเบื้องหลัง
- การเรียก `fetch` คืนค่ากลับมา และจัดตารางการอัปเดต `NormalPriority` สำหรับแบนเนอร์ เนื่องจากสิ่งนี้มีลำดับความสำคัญสูงกว่าตัวอย่างคอมเมนต์ React จะหยุดการเรนเดอร์ตัวอย่างชั่วคราว, ทำงานอัปเดตแบนเนอร์, คอมมิตไปยัง DOM, แล้วจึงกลับมาทำงานเรนเดอร์ตัวอย่างต่อเมื่อมีเวลาว่าง
- เมื่อการโต้ตอบของผู้ใช้และงานที่มีลำดับความสำคัญสูงกว่าเสร็จสิ้นทั้งหมด React จะกลับมาทำงานเรนเดอร์การเลื่อนที่เป็น `NormalPriority` เดิมต่อจากจุดที่ค้างไว้
การหยุด, การจัดลำดับความสำคัญ, และการกลับมาทำงานต่อแบบไดนามิกนี้คือหัวใจสำคัญของการจัดการช่องทางลำดับความสำคัญ มันทำให้มั่นใจได้ว่าการรับรู้ถึงประสิทธิภาพของผู้ใช้จะถูกปรับให้เหมาะสมอยู่เสมอ เพราะการโต้ตอบที่สำคัญที่สุดจะไม่ถูกบล็อกโดยงานเบื้องหลังที่สำคัญน้อยกว่า
ผลกระทบในภาพรวม: มากกว่าแค่ความเร็ว
ประโยชน์ของโมเดลการเรนเดอร์แบบ concurrent ของ React นั้นมีมากกว่าแค่การทำให้แอปพลิเคชันรู้สึกเร็ว มันมีผลกระทบที่จับต้องได้ต่อตัวชี้วัดทางธุรกิจและผลิตภัณฑ์ที่สำคัญสำหรับฐานผู้ใช้ทั่วโลก
- การเข้าถึง (Accessibility): UI ที่ตอบสนองได้ดีคือ UI ที่เข้าถึงได้ง่าย เมื่ออินเทอร์เฟซค้าง มันอาจทำให้ผู้ใช้ทุกคนสับสนและใช้งานไม่ได้ แต่มันเป็นปัญหาอย่างยิ่งสำหรับผู้ที่ต้องพึ่งพาเทคโนโลยีช่วยเหลือ เช่น โปรแกรมอ่านหน้าจอ ซึ่งอาจสูญเสียบริบทหรือไม่ตอบสนอง
- การรักษาผู้ใช้ (User Retention): ในโลกดิจิทัลที่มีการแข่งขันสูง ประสิทธิภาพคือฟีเจอร์อย่างหนึ่ง แอปพลิเคชันที่ช้าและกระตุกนำไปสู่ความหงุดหงิดของผู้ใช้, อัตราการออกจากเว็บที่สูงขึ้น, และการมีส่วนร่วมที่ลดลง ประสบการณ์ที่ลื่นไหลคือความคาดหวังพื้นฐานของซอฟต์แวร์สมัยใหม่
- ประสบการณ์ของนักพัฒนา (Developer Experience): ด้วยการสร้างกลไกการจัดตารางเวลาอันทรงพลังเหล่านี้ไว้ในไลบรารีเอง React ช่วยให้นักพัฒนาสามารถสร้าง UI ที่ซับซ้อนและมีประสิทธิภาพสูงในรูปแบบ declarative ได้มากขึ้น แทนที่จะต้องมาเขียนลอจิก debouncing, throttling, หรือ `requestIdleCallback` ที่ซับซ้อนด้วยตนเอง นักพัฒนาสามารถส่งสัญญาณความตั้งใจไปยัง React ได้ง่ายๆ โดยใช้ API อย่าง `startTransition` ซึ่งนำไปสู่โค้ดที่สะอาดและบำรุงรักษาง่ายขึ้น
ข้อแนะนำที่นำไปปฏิบัติได้สำหรับทีมพัฒนาระดับโลก
- ยอมรับ Concurrency: ตรวจสอบให้แน่ใจว่าทีมของคุณใช้ React 18 และเข้าใจฟีเจอร์ concurrent ใหม่ๆ นี่คือการเปลี่ยนแปลงกระบวนทัศน์
- ระบุ Transitions: ตรวจสอบแอปพลิเคชันของคุณเพื่อหาการอัปเดต UI ที่ไม่เร่งด่วน ครอบการอัปเดต state ที่เกี่ยวข้องด้วย `startTransition` เพื่อป้องกันไม่ให้มันบล็อกการโต้ตอบที่สำคัญกว่า
- เลื่อนการเรนเดอร์ที่หนัก: สำหรับคอมโพเนนต์ที่เรนเดอร์ช้าและขึ้นอยู่กับข้อมูลที่เปลี่ยนแปลงอย่างรวดเร็ว ให้ใช้ `useDeferredValue` เพื่อลดลำดับความสำคัญในการ re-render ของมันและทำให้ส่วนที่เหลือของแอปพลิเคชันยังคงตอบสนองได้ดี
- โปรไฟล์และวัดผล: ใช้ React DevTools Profiler เพื่อดูภาพการเรนเดอร์ของคอมโพเนนต์ของคุณ Profiler ได้รับการอัปเดตสำหรับ concurrent React และสามารถช่วยคุณระบุได้ว่าการอัปเดตใดที่ถูกขัดจังหวะและอันไหนที่ก่อให้เกิดปัญหาคอขวดด้านประสิทธิภาพ
- ให้ความรู้และเผยแพร่: ส่งเสริมแนวคิดเหล่านี้ภายในทีมของคุณ การสร้างแอปพลิเคชันที่มีประสิทธิภาพเป็นความรับผิดชอบร่วมกัน และความเข้าใจร่วมกันเกี่ยวกับ scheduler ของ React เป็นสิ่งสำคัญสำหรับการเขียนโค้ดที่เหมาะสมที่สุด
สรุป
React Fiber และ scheduler ที่ทำงานตามลำดับความสำคัญของมันถือเป็นก้าวกระโดดครั้งสำคัญในวิวัฒนาการของเฟรมเวิร์ก front-end เราได้ย้ายจากโลกของการเรนเดอร์แบบซิงโครนัสที่ปิดกั้น ไปสู่กระบวนทัศน์ใหม่ของการจัดตารางเวลาที่ร่วมมือกันและขัดจังหวะได้ ด้วยการแบ่งงานออกเป็นชิ้นส่วน fiber ที่จัดการได้ และใช้โมเดลเลนที่ซับซ้อนเพื่อจัดลำดับความสำคัญของงานนั้น React สามารถรับประกันได้ว่าการโต้ตอบที่ผู้ใช้เห็นจะถูกจัดการก่อนเสมอ สร้างแอปพลิเคชันที่รู้สึกไหลลื่นและรวดเร็วทันใจ แม้ในขณะที่กำลังทำงานที่ซับซ้อนในเบื้องหลัง
สำหรับนักพัฒนา การทำความเข้าใจแนวคิดอย่าง transitions และ deferred values ไม่ใช่แค่การปรับปรุงประสิทธิภาพทางเลือกอีกต่อไป แต่เป็นความสามารถหลักสำหรับการสร้างเว็บแอปพลิเคชันสมัยใหม่ที่มีประสิทธิภาพสูง ด้วยการทำความเข้าใจและใช้ประโยชน์จากการจัดการช่องทางลำดับความสำคัญของ React คุณสามารถมอบประสบการณ์ผู้ใช้ที่เหนือกว่าให้กับผู้ชมทั่วโลก สร้างอินเทอร์เฟซที่ไม่เพียงแต่ใช้งานได้ แต่ยังน่าใช้งานอย่างแท้จริง